package code.google.jcontainer.resolver;

import code.google.jcontainer.annotation.Resolve;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
import java.util.HashMap;
import java.util.Map;

/**
 * Repository to hold AnnotationResolvers, and provides methods to resolve annotations. 
 *
 * @author <a href="mailto:jfox.young@gmail.com">Young Yang</a>
 */
public class AnnotationResolverRepository {
    private static final Log log = LogFactory.getLog(AnnotationResolverRepository.class);

    /**
     * Annotation Resolvers map, annotationClassName=>AnnotationResolver
     */
    private Map<String, AnnotationResolver> annotationResolvers = new HashMap<String, AnnotationResolver>();

    /**
     * used to determine when to resolve such annotation, by whens & when != 0
     */
    private Map<String, Integer> resolverWhens = new HashMap<String, Integer>();

    /**
     * repository name, equals to container name
     */
    private String name;

    public AnnotationResolverRepository(String name) {
        this.name = name;
    }

    /**
     * return repository name, equals to container name
     */
    public String getName() {
        return name;
    }

    /**
     * determine if a annotation is supported 
     */
    public boolean isSupportAnnotation(Class<? extends Annotation> annotationClass){
        return annotationResolvers.containsKey(annotationClass.getName());
    }

    /**
     * parse and register a supported Annotation, supported annotation will be resolve by Container
     * support annotations is specified by {@link code.google.jcontainer.annotation.Container#supportAnnotations()}
     */
    public void addSupportAnnotationClass(Class<? extends Annotation> annotationClass) {
        // parse @Resolve on the annotation class
        if(!annotationClass.isAnnotationPresent(Resolve.class)){
            throw new ResolverRuntimeException("Annotation " + annotationClass.getName() + " is not annotated by @Resolve");
        }

        Resolve resolve = annotationClass.getAnnotation(Resolve.class);
        Class<? extends AnnotationResolver> resolverClass = resolve.resolverClass();
        registerAnnotation(annotationClass, resolverClass, resolve.when());
    }

    /**
     * parse and register a 3party annotation, after registered, 3party annotation will be treated as supported annotation
     * resolve annotation is specified by {@link code.google.jcontainer.annotation.Container#resolveAnnotations()}
     */
    public void addResolveAnnotation(Resolve resolveAnnotation){
        Class<? extends Annotation> annotationClass = resolveAnnotation.annotationClass();
        Class<? extends AnnotationResolver> resolverClass = resolveAnnotation.resolverClass();
        Resolve.WHEN[] whens = resolveAnnotation.when();
        registerAnnotation(annotationClass, resolverClass, whens);
    }

    /**
     * register a annotation, registered annotation will be managed by Container
     */
    protected void registerAnnotation(Class<? extends Annotation> annotationClass, Class<? extends AnnotationResolver> resolverClass, Resolve.WHEN[] whens){
        // check resolverClass
        Target target = annotationClass.getAnnotation(Target.class);
        ElementType[] elementTypes = target.value();
        for(ElementType elementType : elementTypes) {
            switch (elementType) {
                case METHOD:
                    if(!MethodAnnotationResolver.class.isAssignableFrom(resolverClass)) {
                        throw new ResolverRuntimeException("Annotation " + annotationClass.getName() + " has ElementType.METHOD, but its resolver " + resolverClass.getName() + " doesn't implements " + MethodAnnotationResolver.class.getName());
                    }
                    break;
                case TYPE:
                    if(!ClassAnnotationResolver.class.isAssignableFrom(resolverClass)) {
                        throw new ResolverRuntimeException("Annotation " + annotationClass.getName() + " has ElementType.TYPE, but its resolver " + resolverClass.getName() + " doesn't implements " + ClassAnnotationResolver.class.getName());
                    }
                    break;
                case FIELD:
                    if(!FieldAnnotationResolver.class.isAssignableFrom(resolverClass)) {
                        throw new ResolverRuntimeException("Annotation " + annotationClass.getName() + " has ElementType.FIELD, but its resolver " + resolverClass.getName() + " doesn't implements " + FieldAnnotationResolver.class.getName());
                    }
                    break;
            }
        }

        try {
            AnnotationResolver annotationResolver = resolverClass.newInstance();
            annotationResolvers.put(annotationClass.getName(), annotationResolver);
        }
        catch (Exception e) {
            throw new ResolverRuntimeException("Failed to instantiate resolve class: " + resolverClass, e);
        }

        int when = 0;
        for(Resolve.WHEN _when : whens) {
            when += _when.getIntValue();
        }
        if(when == 0) {
            log.warn("Annotation " + annotationClass.getName() + "'s Rosolve.whens is empty, when do you want to resolve it?");
        }
        resolverWhens.put(annotationClass.getName(), when);
        log.info("Registered annotation: " + annotationClass.getName() + " for Container " + getName());
    }

    /**
     * return annotation resolver for a annotation
     */
    public AnnotationResolver getAnnotationResolver(String annotationClassName) {
        return annotationResolvers.get(annotationClassName);
    }

    /**
     * resolve a method annotation before or after a method invocation
     */
    public void resolveMethodAnnotationAroundMethod(MethodResolveContext methodResolveContext, Resolve.WHEN when ) {
        Annotation methodAnnotation = methodResolveContext.getAnnotation();
        if(isSupportAnnotation(methodAnnotation.annotationType())) {
            int whens = resolverWhens.get(methodAnnotation.annotationType().getName());
            if((whens & when.getIntValue()) !=0)  {
                AnnotationResolver annotationResolver =  getAnnotationResolver(methodAnnotation.annotationType().getName());
                if(when.equals(Resolve.WHEN.BEFORE_METHOD)) {
                    ((MethodAnnotationResolver)annotationResolver).resolveMethodAnnotationBeforeMethod(methodResolveContext);
                }
                else if(when.equals(Resolve.WHEN.AFTER_METHOD)){
                    ((MethodAnnotationResolver)annotationResolver).resolveMethodAnnotationAfterMethod(methodResolveContext);
                }
                else if(when.equals(Resolve.WHEN.AFTER_CONSTRUCT)){
                    ((MethodAnnotationResolver)annotationResolver).resolveMethodAnnotationAfterConstruct(methodResolveContext);
                }
            }
        }
        else {
            log.warn("Annotation " + methodAnnotation.annotationType().getName() + " isn't supported by container: " + getName());
        }
    }

    /**
     * resolve a method annotation after component construction 
     */
    public void resolveMethodAnnotationAroundConstruct(MethodResolveContext methodResolveContext, Resolve.WHEN when){
        Annotation methodAnnotation = methodResolveContext.getAnnotation();
        if(isSupportAnnotation(methodAnnotation.annotationType())) {
            int whens = resolverWhens.get(methodAnnotation.annotationType().getName());
            if((whens & when.getIntValue()) !=0)  {
                AnnotationResolver annotationResolver =  getAnnotationResolver(methodAnnotation.annotationType().getName());
                if(when.equals(Resolve.WHEN.BEFORE_CONSTRUCT)) {
                    ((MethodAnnotationResolver)annotationResolver).resolveMethodAnnotationBeforeConstruct(methodResolveContext);
                }
                else if(when.equals(Resolve.WHEN.AFTER_CONSTRUCT)) {
                    ((MethodAnnotationResolver)annotationResolver).resolveMethodAnnotationAfterConstruct(methodResolveContext);
                }
            }
        }
        else {
            log.info("Annotation " + methodAnnotation.annotationType().getName() + " isn't supported by container: " + getName());
        }
    }

    /**
     * resolve a class annotation before or after a method invocation
     */
    public void resolveClassAnnotationAroundMethod(MethodResolveContext methodResolveContext, Resolve.WHEN when) {
        Annotation classAnnotation = methodResolveContext.getAnnotation();
        if(isSupportAnnotation(classAnnotation.annotationType())) {
            int whens = resolverWhens.get(classAnnotation.annotationType().getName());
            if((whens & when.getIntValue()) !=0) {
                AnnotationResolver annotationResolver = annotationResolvers.get(classAnnotation.annotationType().getName());
                if(when.equals(Resolve.WHEN.BEFORE_METHOD)) {
                    ((ClassAnnotationResolver)annotationResolver).resolveClassAnnotationBeforeMethod(methodResolveContext);
                }
                else if(when.equals(Resolve.WHEN.AFTER_METHOD)){
                    ((ClassAnnotationResolver)annotationResolver).resolveClassAnnotationAfterMethod(methodResolveContext);
                }
            }
        }
        else {
            log.info("Annotation " + classAnnotation.annotationType().getName() + " isn't supported by container: " + getName());
        }

    }

    /**
     * resolve a class annotation before or after component construction
     */
    public void resolveClassAnnotationAroundConstruct(ClassResolveContext classResolveContext, Resolve.WHEN when){
        Annotation classAnnotation = classResolveContext.getAnnotation();
        if(isSupportAnnotation(classAnnotation.annotationType())) {
            int whens = resolverWhens.get(classAnnotation.annotationType().getName());
            if((whens & when.getIntValue()) !=0) {
                AnnotationResolver annotationResolver = annotationResolvers.get(classAnnotation.annotationType().getName());
                if(when.equals(Resolve.WHEN.BEFORE_CONSTRUCT)) {
                    ((ClassAnnotationResolver)annotationResolver).resolveClassAnnotationBeforeConstruct(classResolveContext);
                }
                else if(when.equals(Resolve.WHEN.AFTER_CONSTRUCT)){
                    ((ClassAnnotationResolver)annotationResolver).resolveClassAnnotationAfterConstruct(classResolveContext);
                }
            }
        }
        else {
            log.info("Annotation " + classAnnotation.annotationType().getName() + " isn't an supported by container: " + getName());
        }

    }

    /**
     * resolve a field annotation after component construction
     */
    public void resolveFieldAnnotationAroundConstruct(FieldResolveContext fieldResolveContext, Resolve.WHEN when){
        Annotation fieldAnnotation = fieldResolveContext.getAnnotation();
        if(isSupportAnnotation(fieldAnnotation.annotationType())) {
            int whens = resolverWhens.get(fieldAnnotation.annotationType().getName());
            if((whens & when.getIntValue()) !=0) {
                AnnotationResolver annotationResolver = annotationResolvers.get(fieldAnnotation.annotationType().getName());
                if(when.equals(Resolve.WHEN.BEFORE_CONSTRUCT)) {
                    ((FieldAnnotationResolver)annotationResolver).resolveFieldAnnotationBeforeConstruct(fieldResolveContext);
                }
                else if(when.equals(Resolve.WHEN.AFTER_CONSTRUCT)) {
                    ((FieldAnnotationResolver)annotationResolver).resolveFieldAnnotationAfterConstruct(fieldResolveContext);
                }
            }
        }
        else {
            log.info("Annotation " + fieldAnnotation.annotationType().getName() + " isn't supported by container: " + getName());
        }

    }

}
